Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(http-server): adds http/2 support #4989

Closed
wants to merge 3 commits into from

Conversation

achrinza
Copy link
Member

Signed-off-by: Rifa Achrinza [email protected]

Checklist

👉 Read and sign the CLA (Contributor License Agreement) 👈

  • npm test passes on your machine
  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style guide
  • API Documentation in code was updated
  • Documentation in /docs/site was updated
  • Affected artifact templates in packages/cli were updated
  • Affected example projects in examples/* were updated

👉 Check out how to submit a PR 👈

@emonddr
Copy link
Contributor

emonddr commented Apr 6, 2020

Great start @achrinza . Thank you. :) Do you have mocha tests in mind as well?

Copy link
Contributor

@emonddr emonddr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add test cases. thx.

@achrinza
Copy link
Member Author

achrinza commented Apr 7, 2020

@emonddr That's the plan; though I'm trying to identify what would be suitable tests.

Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for starting the work on adding HTTP/2 support 👍

The way how I see http-server designed, it allows applications to use the same code to setup a listening HTTP/x server, and let http-server implementation to decide which Node.js API (http/https/http2) to use under the hood.

In that light, your proposal to support both RequestListener and Http2RequestListener does not make sense to me, because the application would have to understand whether it's using HTTP/1 or HTTP/2 in order to provide the right request handler.

Personally, I think we should support only the current RequestHandler signature and use HTTP/2 Compatibility API to setup HTTP/2 servers. See the docs here: https://nodejs.org/dist/latest-v12.x/docs/api/http2.html#http2_compatibility_api

In the future, we may "upgrade" this package to require Http2RequestListener only and and use forward-compatible wrapper around HTTP/1 to allow HTTP/2 handlers to deal with HTTP/1 requests. I believe such wrapper has been discussed in Node.js core in the past, but I don't know what was the outcome and whether it's actually possible to implement such wrapper at all.

In case we decide to continue with the direction you proposed originally, I have few comments to consider - see below.

packages/http-server/src/http-server.ts Show resolved Hide resolved
@@ -81,7 +114,7 @@ export class HttpServer {
* @param serverOptions
*/
constructor(
requestListener: RequestListener,
requestListener: RequestListener | Http2RequestListener,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this constructor API sub-optimal, because it allows caller to mix HTTP v1 request listener with HTTP v2 server options and vice versa.

Can we use function overloads to implement more tight type checks please?

class HttpServer {
  // ...
  constructor(
     requestListener: RequestListener,
     serverOptions?: HttpOptions | HttpsOptions
  );

  constructor(
     requestListener: Http2RequestListener,
     serverOptions?: Http2Options | Https2Options
  );

  constructor(
    requestListener: RequestListener | Http2RequestListener,
    serverOptions?: HttpServerOptions,
  ) {
    // the implementation
  }
}

@achrinza
Copy link
Member Author

Thanks for the feedback @bajtos,

let http-server implementation to decide which Node.js API (http/https/http2) to use under the hood.

HTTP/2 isn't backwards-compatible unless we explicitly enable ALPN Negotiation. The main limiting factor is that this only supports HTTPS and will not support plain HTTP/1.

Personally, I think we should support only the current RequestHandler signature and use HTTP/2 Compatibility API to setup HTTP/2 servers.

I have concerns that this would be limiting users from customizing certain properties such as the ALPN Negotiation. As an independent package, users should be able to use all features from the Node.js API (whether directly or indirectly), otherwise this would be a limiting factor.

In the future, we may "upgrade" this package to require Http2RequestListener only and and use forward-compatible wrapper around HTTP/1 to allow HTTP/2 handlers to deal with HTTP/1 requests.

Sounds like a good idea, though I'm not too sure on what the implications would be.

WDYT?

@bajtos
Copy link
Member

bajtos commented May 7, 2020

I am new to HTTP/2 and still learning the details. As I result, I did not fully understand the difference between different HTTP/2 server APIs and their type descriptions 🙈 I think most of my comments above can be ignored.

Here is my current understanding of what options Node.js core provides:

  1. HTTP/1 server, where the request handler has signature function (req, res).
  2. HTTP/2 server compatibility mode, where the request handler has signature function (req, res) but the types for req and res are slightly different from HTTP/1, at least as far as @types/node provides them. Such server can be setup to accept HTTP/2 connections only, or allow both HTTP/1 and HTTP/2 clients.
  3. HTTP/2 native server, where the request handler has signature function(stream, headers).

Because a native HTTP/2 handler has very different signature, I think it's out of scope of HttpServer for now and we want to focus on supporting function (req, res) only. Do you @achrinza agree?

Let's agree on what scenarios we want support, what scenarios we do not want to support, and what edge cases we need to consider.

I see three types of handlers to consider:

  1. The current request handler based on HTTP/1 types.

    function v1handler(req: http.IncomingMessage, res: http.ServerResponse) {
      res.writeHead(200, { 'content-type': 'application/json' });
      res.end(JSON.stringify({httpVersion: req.httpVersion}));
    }
  2. A handler that support HTTP/2 requests only.

    function v2handler(req: http2.Http2ServerRequest, res: http2.Http2ServerResponse) {
        res.writeHead(200, { 'content-type': 'application/json' });
        res.end(JSON.stringify({
          httpVersion: req.httpVersion,
          alpnProtocol: req.stream.session.alpnProtocol,
        }));
    }
  3. A handler that supports both HTTP/1 and HTTP/2 requests.

    function v1v2(req /* what's the type? */, res /* what's the type? */) {
      res.writeHead(200, { 'content-type': 'application/json' });
      const data = {{httpVersion: req.httpVersion};
      if (req.httpVersion === '2.0' ) {
         data.alpnProtocol = req.stream.session.alpnProtocol;
      }
      res.end(JSON.stringify(data));
    }

And three kinds of server configurations:

  1. HTTP/1 only: {protocol: 'http'} (or https)
  2. HTTP/2 only: {protocol: 'http2'} (or secure HTTP2)
  3. HTTP/1+2: {protocol: 'https2', allowHTTP1: true}

That gives us 9 different scenarios. To keep things simpler, I am proposing for now to leave out v2handler from the matrix and treat HTTP/2 and HTTP/1+2 as the same, to get 4 scenarios that I consider as most important.

Scenario A - HTTP/1 server and handler

This is the current status.

const server = new HttpServer(v1handler, {
  protocol: 'http',
  // ...
});

Scenario B - HTTP/1+2 server and HTTP/1 handler

This will immediately enable all existing LoopBack applications to start accepting HTTP/2 clients, with no further changes necessary in @loopback/rest or application code (besides the server configuration).

const server = new HttpServer(v1handler, {
  protocol: 'http2s',
  allowHTTP1: true,
  // ...
});

Scenario C - HTTP/1+2 server and HTTP/1+2 handler

This is an upgrade that will eventually allow LB applications to start using HTTP/2 features if the client supports them, after we make the necessary changes in @loopback/rest.

const server = new HttpServer(v1v2handler, {
  protocol: 'http2s',
  allowHTTP1: true,
  // ...
});

Scenario D - HTTP/1 server and HTTP/1+2 handler HTTP/1 handler

This allows LB applications supporting HTTP/2 clients to be able to run within an HTTP/1 server.

const server = new HttpServer(v1v2handler, {
  protocol: 'http',
  // ...
});

The important part in all of those scenarios above is that the handler signature remains the same independently of what server configuration is provided. We need to find a way how to write our type definitions to allow such usage patterns.

@achrinza Is this in line with what you were thinking of when you started this pull request?

I find it quite difficult to build the full context needed to provide a meaningful review. It would help me tremendously if we could split this pull request into smaller chunks.

I am proposing the following baby steps, where each step provides a meaningful new feature that can be immediately used by our users.

Let me start by illustrating the current status:

layer supports note
user-implemented controllers HTTP/1 What types of request and response can be injected to controllers?
@loopback/rest HTTP/1 How is the routing, request parsing and response writing implemented?
http-server handlers HTTP/1 What kind of handlers can be given to HttpServer?
http-server listeners HTTP/1 What kind of HTTP servers can be created?

Step 1

Allow existing applications to start an HTTP/1+2 (and HTTP/2) server. See "Scenario B" for a test case.

layer supports
user-implemented controllers HTTP/1
@loopback/rest HTTP/1
http-server handlers HTTP/1
http-server listeners HTTP/1 or HTTP/2 or HTTP/1+2

This step can be done by the current pull request. I think it should be pretty straightforward, we just need to add new server-options type and verify that a HTTP/1 handler can be used with a HTTP/2 server.

I think this is fully supported according to Node.js docs, but it seems like @types/node does not fully reflect that.

  • HttpServerRequest cannot be assigned to http.IncomingMessage because the following properties are missing: aborted, httpVersionMajor, httpVersionMinor, connection. https://nodejs.org/api/http2.html#http2_class_http2_http2serverrequest mention all four properties as present.
  • HttpServerResponse cannot be assigned to HttpServerResponse because the following properties are missing: assignSocket, detachSocket, upgrading, chunkedEncoding, and 13 more. Some of these properties seem to be internal properties that are not part of the public API, in which case I think it's fine to ignore them. We should verify all of them, I checked only upgrading and chunkedEncoding.

Quoting from https://nodejs.org/api/http2.html#http2_compatibility_api

The Compatibility API has the goal of providing a similar developer experience of HTTP/1 when using HTTP/2, making it possible to develop applications that support both HTTP/1 and HTTP/2. This API targets only the public API of the HTTP/1. However many modules use internal methods or state, and those are not supported as it is a completely different implementation.

Step 2

Enable HTTP/1+2 handler signature. See "Scenario C" and "Scenario D" for test cases.

layer supports
user-implemented controllers HTTP/1
@loopback/rest HTTP/1
http-server handlers HTTP/1 or HTTP/1+2
http-server listeners HTTP/1 or HTTP/2 or HTTP/1+2

I am expecting that we will need to go deep into TypeScript language features and @types/node to find a way how to describe the handler signature and HttpServer constructor arguments in such way that will make them easy to use for HttpServer consumers. I prefer to move this part to a new pull request, after the first step is landed.

Step 2.5

Allow HTTP/2 only request handler signatures for servers that are not going to use enableHttp1: true flag. I am not sure if this feature is needed at all and how easy/difficult it would be to correctly detect the situation when the handler is HTTP/2 but the server config was accidentally provided with enableHttp1: true flag.

Step 3 (longer term)

Upgrade @loopback/rest to use HTTP/2 handler signature, hopefully this can be done in a backwards-compatible manner.

I am not sure how easy this is going to be, because we use Express under the hood and I don't know if Express supports HTTP/2 yet. There used to be major issues because of the way how Express patches the HTTP request & response objects and/or prototypes.

layer supports
user-implemented controllers HTTP/1
@loopback/rest HTTP/1+2*
http-server handlers HTTP/1 or HTTP/2 or HTTP/1+2
http-server listeners HTTP/1 or HTTP/2 or HTTP/1+2

Step 4 (longer term)

Update signatures of types used by applications (e.g. RestBindings.Http.REQUEST, sequence action types) to use the new HTTP/2 types. I think this is going to be a breaking change because of the differences between HTTP/1 and HTTP/2 types provided by @types/node.

layer supports
user-implemented controllers HTTP/1+2
@loopback/rest HTTP/1+2
http-server handlers HTTP/1 or HTTP/2 or HTTP/1+2
http-server listeners HTTP/1 or HTTP/2 or HTTP/1+2

That's how I am thinking about approaching HTTP/2 support. What do you think, @achrinza @emonddr @hacksparrow?

export interface Https2Options
extends BaseHttpOptions,
http2.SecureServerOptions {
protocol: 'https2';
Copy link
Member

@bajtos bajtos May 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: can we please use http2s ("http2" + "secure")? I find https2 weird to read.

Suggested change
protocol: 'https2';
protocol: 'http2s';

Note that HTTP/2 spec puts the version number before additional flags too, e.g h2c for clear-text connections (not hc2). See e.g. https://stackoverflow.com/a/37373039/69868

Similarly the interface should be called Http2SecureOptions or perhaps Http2sOptions if you prefer.

@bajtos
Copy link
Member

bajtos commented May 7, 2020

Scenario B - HTTP/1+2 server and HTTP/1 handler

This will immediately enable all existing LoopBack applications to start accepting HTTP/2 clients, with no further changes necessary in @loopback/rest or application code (besides the server configuration).

How naive I was. Express still does not support HTTP/2, see expressjs/express#3730. Because we are internally using Express, it's likely that HTTP/2 will not work in LoopBack 4. I wish we picked a more modern framework as the base, e.g fastify 🤷‍♂️

@jannyHou
Copy link
Contributor

Thank you @achrinza Good proposal 👍
I am still learning the rest server and also new to http2, seems we need a new requestHandler for the http2 client, the current one is an express app https://github.com/strongloop/loopback-next/blob/52dcca012c9ab927d6764845f2a22e2f874bbae3/packages/rest/src/rest.server.ts#L241 which doesn't support http2 yet.

@dhmlau
Copy link
Member

dhmlau commented Aug 19, 2020

We just switch the contribution method from CLA to DCO, making your contribution easier in the future. Please sign the commits with DCO by amending your commit messages with -s flag and push the changes again. If you're using the command line, you can:

git commit --amend -s
git push --force-with-lease

Please refer to this docs page for details. Thanks!

@achrinza
Copy link
Member Author

Closing this issue as it's not actionable.

@achrinza achrinza closed this Dec 11, 2020
@vaibhavkumar-sf
Copy link
Contributor

vaibhavkumar-sf commented Jul 1, 2022

@achrinza , can you please re-open it, we need HTTP2 support in LB4

Is there any way to do it? or is there any future plan for supporting it?

reference : https://strongloop.com/strongblog/serving-a-progressive-web-app-from-loopback/
#2078

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants